# has the basic card functions shared for levels and challenges

import random

NUMBER_OF_RANKS = 13
NUMBER_OF_SUITS = 4
NUMBER_OF_CARDS = NUMBER_OF_RANKS * NUMBER_OF_SUITS
NUMBER_OF_CARDS_WITH_JOKERS = NUMBER_OF_CARDS + 2

####################################################
### This section is for the basic card info ###
####################################################

def get_color(number):
    return int(number / 26)

## @returns the suit number Spade=0, Club=1, Diamond=2, Heart=3
def get_suit(number):
    return int(number / 13) 

## @returns the rank number Two=0, Three=1, ... King=11, Ace=12
def get_rank(number):
    return number % 13

class Combo:
    SINGLE=0
    PAIR=1
    PAIR_JACKS=2
    TWO_PAIRS=3
    THREE_OF_A_KIND=4
    STRAIGHT=5
    FLUSH=6
    FULL_HOUSE=7
    FOUR_OF_A_KIND=8
    STRAIGHT_FLUSH=9
    ROYAL_STRAIGHT_FLUSH=10

####################################################
### This section is for printing cards as strings###
####################################################

RANK_NAMES = {0:"Two",
              1:"Three",
              2:"Four",
              3:"Five",
              4:"Six",
              5:"Seven",
              6:"Eight",
              7:"Nine",
              8:"Ten",
              9:"Jack",
              10:"Queen",
              11:"King",
              12:"Ace"}

RANK_SYMBOLS = {0:"2",
              1:"3",
              2:"4",
              3:"5",
              4:"6",
              5:"7",
              6:"8",
              7:"9",
              8:"10",
              9:"J",
              10:"Q",
              11:"K",
              12:"A"}

SUIT_NAMES = {0:"Spade",
              1:"Club",
              2:"Diamond",
              3:"Heart"}

def get_rank_name(rank):
    return RANK_NAMES.get(rank, "A non-existent rank") 

def get_rank_symbol(rank):
    return RANK_SYMBOLS.get(rank, "A non-existent rank") 

def get_rank_name_plural(rank):
    if rank == 4:
        return "Sixes"
    else:
        return RANK_NAMES.get(rank, "Not a rank") + "s"

def get_suit_name(suit):
    return SUIT_NAMES.get(suit, "A non-existent suit")

SHORT_COMBO_STRING_FUNCTIONS = {0:lambda x: get_rank_symbol(x),
              1:lambda x: "%s %s" % (get_rank_symbol(x), get_rank_symbol(x)),
              2:lambda x: "%s %s" % (get_rank_symbol(x), get_rank_symbol(x)),
              3:lambda x, y: "%s %s %s %s" % (get_rank_symbol(x), \
                    get_rank_symbol(x), get_rank_symbol(y), get_rank_symbol(y)),
              4:lambda x: "%s %s %s" % (get_rank_symbol(x), get_rank_symbol(x), \
                    get_rank_symbol(x)),
              5:lambda x: "Straight to %s" % get_rank_symbol(x),
              6:lambda x, y, z, v, w: "Flush (%s, %s, %s, %s, %s)" % \
                    (get_rank_symbol(x), get_rank_symbol(y), get_rank_symbol(z), \
                     get_rank_symbol(v), get_rank_symbol(w)),
              7:lambda x, y: "%s %s %s %s %s" % (get_rank_symbol(x), \
                    get_rank_symbol(x), get_rank_symbol(x), get_rank_symbol(y), \
                    get_rank_symbol(y)),
              8:lambda x: "Four %s" % get_rank_name_plural(x),
              9:lambda x: "Straight flush to %s" % get_rank_symbol(x),
              10:"ROYAL STRAIGHT FLUSH"}

COMBO_STRING_FUNCTIONS = {0:lambda x: get_rank_name(x),
              1:lambda x: "Pair of %s" % get_rank_name_plural(x),
              2:lambda x: "Pair of %s" % get_rank_name_plural(x),
              3:lambda x, y: "Two pairs %s and %s" % \
                    (get_rank_name_plural(x), get_rank_name_plural(y)),
              4:lambda x: "Three %s" % get_rank_name_plural(x),
              5:lambda x: "Straight %s high" % get_rank_name(x),
              6:lambda x, y, z, v, w: "Flush %s, %s, %s, %s, %s" % \
                    (get_rank_name(x), get_rank_name(y), get_rank_name(z), \
                     get_rank_name(v), get_rank_name(w)),
              7:lambda x, y: "Three %s and two %s (a full house)" % \
                    (get_rank_name_plural(x), get_rank_name_plural(y)),
              8:lambda x: "Four of a kind %s" % get_rank_name_plural(x),
              9:lambda x: "Straight flush %s high" % get_rank_name(x),
              10:"ROYAL STRAIGHT FLUSH"}

def print_two_card_combos(combos):
    # If 2 singles
    if combos[0][0] == 0:
        single_func = SHORT_COMBO_STRING_FUNCTIONS.get(0)
        connector = "%s %s"
        return connector % (single_func(*combos[0][1]), \
                              single_func(*combos[1][1]))
    elif combos[0][0] == 1:
        pair_func = SHORT_COMBO_STRING_FUNCTIONS.get(1)
        return pair_func(*combos[0][1])
    else:
        print "This should never happen, two cards only"

# Get a printable string describing combos 
def print_five_card_combos(combos):
    val = None
    # If just one combo
    if combos[0][0] in (5, 6, 7, 9):
        func = SHORT_COMBO_STRING_FUNCTIONS.get(combos[0][0])
        val = func(*combos[0][1])
    elif combos[0][0] in (3, 8):
        func = SHORT_COMBO_STRING_FUNCTIONS.get(combos[0][0])
        single_func = SHORT_COMBO_STRING_FUNCTIONS.get(0)
        val = "%s %s" % (func(*combos[0][1]), \
                               single_func(*combos[1][1]))
    elif combos[0][0] in (0, 1, 2, 4):
        func = SHORT_COMBO_STRING_FUNCTIONS.get(combos[0][0])
        val = func(*combos[0][1])
        single_func = SHORT_COMBO_STRING_FUNCTIONS.get(0)
        for i in xrange(len(combos)-1):
            val += " %s" % single_func(*combos[i+1][1])
    else:
        print "This is an invalid combo"
        raise
    return val

##################################################
### This section is for some combination logic ###
##################################################

## @description Given an array of cards
## @returns the highest card of a straight found or None
def has_straight(cards):
    rank_buckets = [0] * NUMBER_OF_RANKS
    for i in xrange(len(cards)):
        rank = get_rank(cards[i])
        rank_buckets[rank] += 1
    card_counter = 0
    highest_card = None
    # Ace can be first
    if rank_buckets[NUMBER_OF_RANKS-1] > 0:
        card_counter += 1
    for i in xrange(NUMBER_OF_RANKS):
        if rank_buckets[i] > 0:
            card_counter += 1
            if card_counter >= 5:
                highest_card = i
        else:
            card_counter = 0
    return highest_card

## Given an array of up to 9 cards, returns the top Combo they contain
def get_top_combo(cards):
    suit_buckets = [[] for i in range(NUMBER_OF_SUITS)]
    rank_buckets = [0] * NUMBER_OF_RANKS
    for i in xrange(len(cards)):
        suit_list = suit_buckets[get_suit(cards[i])]
        suit_list.append(cards[i])
        rank = get_rank(cards[i])
        rank_buckets[rank] += 1
        
    # For reuse later
    flush_suit = None
    
    # Checking for a straight flush
    for i in xrange(NUMBER_OF_SUITS):
        if len(suit_buckets[i]) >= 5:
            flush_suit = i        
            # We found a flush, straight flush?
            highest_rank = has_straight(suit_buckets[i])
            if highest_rank is not None:
                if highest_rank == NUMBER_OF_RANKS - 1:
                    return Combo.ROYAL_STRAIGHT_FLUSH
                else:
                    return Combo.STRAIGHT_FLUSH
        
    # To reuse later if a higher combination wasn't found
    triple_rank_int = None
    double_rank_int = None
    second_double_rank_int = None
        
    # Checking for four of a kind
    reversed_range = range(NUMBER_OF_RANKS)
    reversed_range.reverse()
    for i in reversed_range:
        if rank_buckets[i] > 4:
            print "Five of a kind? Something is wrong!"
            raise
        if rank_buckets[i] == 4:
            return Combo.FOUR_OF_A_KIND
        elif rank_buckets[i] == 3 and triple_rank_int is None:
            triple_rank_int = i
        elif rank_buckets[i] >= 2 and double_rank_int is None:
            double_rank_int = i
        elif rank_buckets[i] == 2 and second_double_rank_int is None:
            second_double_rank_int = i
    
    # Checking for full house
    if triple_rank_int is not None and double_rank_int is not None:
        return Combo.FULL_HOUSE

    # Checking for a flush
    if flush_suit is not None:
        return Combo.FLUSH
    
    # Checking for a straight
    highest_rank = has_straight(cards)
    if highest_rank is not None:
        return Combo.STRAIGHT
    
    # Checking for a triple
    if triple_rank_int is not None:
        return Combo.THREE_OF_A_KIND
    
    # Checking for two pairs
    if double_rank_int is not None and second_double_rank_int is not None:
        return Combo.TWO_PAIRS
    
    # Checking for one pair
    if double_rank_int is not None:
        if double_rank_int >= 9:
            return Combo.PAIR_JACKS
        else:
            return Combo.PAIR
    
    return Combo.SINGLE

## @args cards - a list of cards passed in (numbers) 
## @args number - a number of kickers needed
## @args exclude_list - a list of ranks not to include
def get_kickers(cards, number, exclude_list=[]):
    lst = []
    rank_buckets = [0] * NUMBER_OF_RANKS
    for i in xrange(len(cards)):
        rank = get_rank(cards[i])
        rank_buckets[rank] += 1
    
    kickers_found = 0
    rank_counter = NUMBER_OF_RANKS - 1
    while kickers_found < number:
        if (rank_buckets[rank_counter] > 0 and \
            (not rank_counter in exclude_list)):
            lst.append([Combo.SINGLE, [rank_counter]])
            kickers_found += 1
        rank_counter -= 1
    return lst

def get_two_card_combos(cards):
    lst = []
    rank0 = get_rank(cards[0])
    rank1 = get_rank(cards[1])
    if rank0 == rank1:
        lst.append([Combo.PAIR, [rank0]])
    elif rank0 > rank1:
        lst.append([Combo.SINGLE, [rank0]])
        lst.append([Combo.SINGLE, [rank1]])
    else:
        lst.append([Combo.SINGLE, [rank1]])
        lst.append([Combo.SINGLE, [rank0]])
    return lst

## Given 2, 5, 6, or 7 cards, returns top 
## 5 cards (if available) as a list of Combos
def get_combos_with_indicators(cards):
    # This is much simpler, let's do it in a separate
    # short function
    if len(cards) < 5:
        return get_two_card_combos(cards)
    
    lst = []
    suit_buckets = [[] for i in range(NUMBER_OF_SUITS)]
    rank_buckets = [0] * NUMBER_OF_RANKS
    for i in xrange(len(cards)):
        rank = get_rank(cards[i])
        rank_buckets[rank] += 1
        suit_list = suit_buckets[get_suit(cards[i])]
        suit_list.append(rank)
        
    # For reuse later
    flush_suit = None
        
    # Checking for a straight flush
    for i in xrange(NUMBER_OF_SUITS):
        if len(suit_buckets[i]) >= 5:
            flush_suit = i        
            # We found a flush, straight flush?
            highest_rank = has_straight(suit_buckets[i])
            if highest_rank is not None:
                lst.append([Combo.STRAIGHT_FLUSH, [highest_rank]])
                return lst
        
    # To reuse later if a higher combination wasn't found
    triple_rank_int = None
    double_rank_int = None
    second_double_rank_int = None
        
    # Checking for four of a kind
    reversed_range = range(NUMBER_OF_RANKS)
    reversed_range.reverse()
    for i in reversed_range:
        if rank_buckets[i] > 4:
            print "Five of a kind? Something is wrong!"
            raise
        if rank_buckets[i] == 4:
            lst.append([Combo.FOUR_OF_A_KIND, [i]])
            kicker_list = get_kickers(cards, 1, [i])
            lst.extend(kicker_list)
            return lst
        elif rank_buckets[i] == 3 and triple_rank_int is None:
            triple_rank_int = i
        elif rank_buckets[i] >= 2 and double_rank_int is None:
            double_rank_int = i
        elif rank_buckets[i] == 2 and second_double_rank_int is None:
            second_double_rank_int = i
    
    # Checking for full house
    if triple_rank_int is not None and double_rank_int is not None:
        lst.append([Combo.FULL_HOUSE, [triple_rank_int, double_rank_int]])
        return lst

    # Checking for a flush
    if flush_suit is not None:
        ranks = sorted(suit_buckets[flush_suit], reverse=True)
        lst.append([Combo.FLUSH, ranks[:5]])
        return lst
    
    # Checking for a straight
    highest_rank = has_straight(cards)
    if highest_rank is not None:
        lst.append([Combo.STRAIGHT, [highest_rank]])
        return lst
    
    # Checking for a triple
    if triple_rank_int is not None:
        lst.append([Combo.THREE_OF_A_KIND, [triple_rank_int]])
        kicker_list = get_kickers(cards, 2, [triple_rank_int])
        lst.extend(kicker_list)
        return lst
    
    # Checking for two pairs
    if double_rank_int is not None and second_double_rank_int is not None:
        lst.append([Combo.TWO_PAIRS, [double_rank_int, second_double_rank_int]])
        kicker_list = get_kickers(cards, 1, [double_rank_int, second_double_rank_int])
        lst.extend(kicker_list)
        return lst
    
    # Checking for one pair
    if double_rank_int is not None:
        lst.append([Combo.PAIR, [double_rank_int]])
        kicker_list = get_kickers(cards, 3, [double_rank_int])
        lst.extend(kicker_list)
        return lst
    
    lst = get_kickers(cards, 5)
    return lst


def cmp_two_card_hand(known_hand, cards):
    # Should be two cards
    cmp_value = cmp(*cards)
    if known_hand[0][0] == Combo.PAIR:
        if cmp_value != 0:
            return 1
        else:
            return cmp(known_hand[0][1][0], cards[0])
    else:
        if cmp_value == 0:
            return -1
        else:
            cmp_value = cmp(known_hand[0][1][0], max(*cards))
            if cmp_value != 0:
                return cmp_value
            else:
                return cmp(known_hand[1][1][0], min(*cards))

# This is a shortcut for get_combos_with_indicators followed by
# compare_combos_with_indicators for when you know one hand as
# you are calculating the other
# Returns >0 if the known_hand is higher,
# 0 if it's a tie
# <0 if the hand being calculated is higher
def cmp_hand(known_hand, cards):
    # This is much simpler, let's do it in a separate
    # short function
    if len(cards) < 5:
        return cmp_two_card_hand(known_hand, cards)
    
    suit_buckets = [[] for i in range(NUMBER_OF_SUITS)]
    rank_buckets = [0] * NUMBER_OF_RANKS
    for i in xrange(len(cards)):
        rank = get_rank(cards[i])
        rank_buckets[rank] += 1
        suit_list = suit_buckets[get_suit(cards[i])]
        suit_list.append(rank)
        
    # For reuse later
    flush_suit = None
        
    # Checking for a straight flush
    for i in xrange(NUMBER_OF_SUITS):
        if len(suit_buckets[i]) >= 5:
            flush_suit = i        
            # We found a flush, straight flush?
            highest_rank = has_straight(suit_buckets[i])
            if highest_rank is not None:
                cmp_value = cmp(known_hand[0][0], Combo.STRAIGHT_FLUSH)
                if cmp_value != 0:
                    return cmp_value
                else:
                    return cmp(known_hand[0][1][0], highest_rank)
    
    # To reuse later if a higher combination wasn't found
    triple_rank_int = None
    double_rank_int = None
    second_double_rank_int = None
        
    # Checking for four of a kind
    reversed_range = range(NUMBER_OF_RANKS)
    reversed_range.reverse()
    for i in reversed_range:
        if rank_buckets[i] > 4:
            print "Five of a kind? Something is wrong!"
            raise
        if rank_buckets[i] == 4:
            cmp_value = cmp(known_hand[0][0], Combo.FOUR_OF_A_KIND)
            if cmp_value != 0:
                return cmp_value
            else:
                cmp_value = cmp(known_hand[0][1][0], i)
                if cmp_value != 0:
                    return cmp_value
                else:
                    kicker_list = get_kickers(cards, 1, [i])
                    return cmp(known_hand[1][1][0], kicker_list[0][1][0])
        elif rank_buckets[i] == 3 and triple_rank_int is None:
            triple_rank_int = i
        elif rank_buckets[i] >= 2 and double_rank_int is None:
            double_rank_int = i
        elif rank_buckets[i] == 2 and second_double_rank_int is None:
            second_double_rank_int = i
    
    # Do we need more?
    if known_hand[0][0] > Combo.FULL_HOUSE:
        return 1
    # Checking for full house
    if triple_rank_int is not None and double_rank_int is not None:
        cmp_value = cmp(known_hand[0][0], Combo.FULL_HOUSE)
        if cmp_value != 0:
            return cmp_value
        else:
            cmp_value = cmp(known_hand[0][1][0], triple_rank_int)
            if cmp_value != 0:
                return cmp_value
            else:
                return cmp(known_hand[0][1][1], double_rank_int)

    # Do we need more?
    if known_hand[0][0] > Combo.FLUSH:
        return 1
    # Checking for a flush
    if flush_suit is not None:
        ranks = sorted(suit_buckets[flush_suit], reverse=True)
        cmp_value = cmp(known_hand[0][0], Combo.FLUSH)
        if cmp_value != 0:
            return cmp_value
        else:
            for i in xrange(5):
                cmp_value = cmp(known_hand[0][1][i], ranks[i])
                if cmp_value != 0:
                    return cmp_value
            return 0
    
    # Do we need more?
    if known_hand[0][0] > Combo.STRAIGHT:
        return 1
    # Checking for a straight
    highest_rank = has_straight(cards)
    if highest_rank is not None:
        cmp_value = cmp(known_hand[0][0], Combo.STRAIGHT)
        if cmp_value != 0:
            return cmp_value
        else:
            return cmp(known_hand[0][1][0], highest_rank)
    
    # Do we need more?
    if known_hand[0][0] > Combo.THREE_OF_A_KIND:
        return 1
    # Checking for a triple
    if triple_rank_int is not None:
        cmp_value = cmp(known_hand[0][0], Combo.THREE_OF_A_KIND)
        if cmp_value != 0:
            return cmp_value
        else:
            cmp_value = cmp(known_hand[0][1][0], triple_rank_int)
            if cmp_value != 0:
                return cmp_value
            else:
                kicker_list = get_kickers(cards, 2, [triple_rank_int])
                for i in xrange(2):
                    cmp_value = cmp(known_hand[i+1][1][0], kicker_list[i][1][0])
                    if cmp_value != 0:
                        return cmp_value
                return 0
    
    # Do we need more?
    if known_hand[0][0] > Combo.TWO_PAIRS:
        return 1
    # Checking for two pairs
    if double_rank_int is not None and second_double_rank_int is not None:
        cmp_value = cmp(known_hand[0][0], Combo.TWO_PAIRS)
        if cmp_value != 0:
            return cmp_value
        else:
            cmp_value = cmp(known_hand[0][1][0], double_rank_int)
            if cmp_value != 0:
                return cmp_value
            cmp_value = cmp(known_hand[0][1][1], second_double_rank_int)
            if cmp_value != 0:
                return cmp_value
            kicker_list = get_kickers(cards, 1, [double_rank_int, second_double_rank_int])
            return cmp(known_hand[1][1][0], kicker_list[0][1][0])
    
    # Do we need more?
    if known_hand[0][0] > Combo.PAIR:
        return 1
    # Checking for one pair
    if double_rank_int is not None:
        cmp_value = cmp(known_hand[0][0], Combo.PAIR)
        if cmp_value != 0:
            return cmp_value
        else:
            cmp_value = cmp(known_hand[0][1][0], double_rank_int)
            if cmp_value != 0:
                return cmp_value
            kicker_list = get_kickers(cards, 3, [double_rank_int])
            for i in xrange(3):
                cmp_value = cmp(known_hand[i+1][1][0], kicker_list[i][1][0])
                if cmp_value != 0:
                    return cmp_value
            return 0
    
    # Do we need more?
    if known_hand[0][0] > Combo.SINGLE:
        return 1
    kicker_list = get_kickers(cards, 5)
    for i in xrange(5):
        cmp_value = cmp(known_hand[i][1][0], kicker_list[i][1][0])
        if cmp_value != 0:
            return cmp_value
    return 0

## @return: >0, 0, <0 if comboList1 is greater, equal, less
## ComboList with indicators is a list of Combos with Indicators
## each element of comboList is a list of two elements:
## first element being the Combo rank, second element being a list
## of ranks for that combo 
def cmp_combos(combo_list1, combo_list2):
    for i in xrange(len(combo_list1)):
        cmp_value = cmp(combo_list1[i][0], combo_list2[i][0])
        if cmp_value != 0:
            return cmp_value
        else:
            for j in xrange(len(combo_list1[i][1])):
                cmp_value = cmp(combo_list1[i][1][j], combo_list2[i][1][j])
                if cmp_value != 0:
                    return cmp_value
    # If we reached here, the two combos are equal
    return 0

def get_result(my_cards, table_cards, your_cards):
    my_combos = get_combos_with_indicators(my_cards + table_cards)
    your_combos = get_combos_with_indicators(your_cards + table_cards)
    return cmp_combos(your_combos, my_combos)

class CardDeck(object):
    
    _cards_available = []
    
    def __init__(self):
        self._cards_available = range(NUMBER_OF_CARDS)
        
    def deal_card(self, card_holder):
        if len(self._cards_available) > 0:
            random_number = random.randint(0, len(self._cards_available) - 1)
            card_id = self._cards_available[random_number]
            self._cards_available.remove(card_id)
            card_holder.add_card(card_id)
        else:
            print "We ran out of card apparently ..."
            raise
        
class CardHolder(object):
    
    def __init__(self):
        self._cards = []
    
    def add_card(self, card):
        self._cards.append(card)
        
    def get_cards(self):
        return self._cards

class Table(CardHolder):
    pass
    
class Player(CardHolder):
    pass